package com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import com.db4o.internal.odbgen.StringUtils;
import com.db4o.internal.odbgen.plugins.java.maingenerator.XmlSchemaElement;
import com.db4o.odbgen.OdbgenException;
import com.db4o.odbgen.plugins.xmlschema.Element;
import com.db4o.odbgen.plugins.xmlschema.XmlSchema;


/**
 * 
 * Used to generate {@link TypedXmlSchemaGeneratorStep1} given {@link XmlSchema}. 
 * @author liviug
 */
class TypedXmlSchemaGeneratorStep1 {
    private XmlSchema _xmlSchema;
    private String _rootPackage;

    /**
     * Creates a {@link TypedXmlSchema} generator.
     * @param xmlSchema The XmlSchema used to generate {@link TypedXmlSchema}.
     * @param rootPackage All types defined within XmlSchema will be prefixed
     * with this.
     */
    public TypedXmlSchemaGeneratorStep1(XmlSchema xmlSchema, String rootPackage){
        this._xmlSchema = xmlSchema;
        this._rootPackage = rootPackage;
    }
    
    /**
     * Generates the information necessary to build a {@link TypedXmlSchema}.
     * @return A map of type declarations. The key is the qualified name of the type.
     * @throws OdbgenException
     */
    public TypeDeclarationCollection generate() throws OdbgenException{
        //The key represents the qualified name of the task.
        TypeDeclarationCollection res = new TypeDeclarationCollection();

        //Generate classes.
        Map<TSClass, String> superclasses = new Hashtable<TSClass, String>();
        Map<TSClass, List<String>> interfaces = new Hashtable<TSClass, List<String>>();
        for(Element elem : this._xmlSchema.getRootElements(XmlSchemaElement.CLASS.getName())){
            try{
                TSClass c = this.buildClass(elem, superclasses, interfaces);
                res.add(c);
            }
            catch(Exception e){
                throw new OdbgenException(
                        e, 
                        "Error while parsing class in XmlSchema file '%s'.",
                        elem.getFilePath()
                        );
            }
        }

        //Generate interfaces.
        for(Element elem : this._xmlSchema.getRootElements(XmlSchemaElement.INTERFACE.getName())){
            try{
                TSInterface i = this.buildInterface(elem);
                res.add(i);
            }
            catch(Exception e){
                throw new OdbgenException(
                        e, 
                        "Error while parsing interface in XmlSchema file '%s'.",
                        elem.getFilePath()
                        );
            }
        }

        //Generate enums.
        for(Element elem : this._xmlSchema.getRootElements(XmlSchemaElement.ENUM.getName())){
            try{
                TSEnum e = this.buildEnum(elem);
                res.add(e);
            }
            catch(Exception e){
                throw new OdbgenException(
                        e, 
                        "Error while parsing enum in XmlSchema file '%s'.",
                        elem.getFilePath()
                        );
            }
        }
        
        //Update the superclasses and interfaces of each class, now we know all classes
        //and interfaces inside XmlSchema.
        this.updateSuperclassesAndInterfaces(res, superclasses, interfaces);
        
        return res;
    }

    /**
     * Used to update all classes with the propper superclasses and interfaces.
     * @param typeDeclarations All declarations inside XmlSchema.
     * @param superclasses The map between each and it's superclass.
     * @param interfaces The map between each class and it's interfaces.
     * @throws OdbgenException 
     */
    private void updateSuperclassesAndInterfaces(
            TypeDeclarationCollection typeDeclarations,
            Map<TSClass, String> superclasses,
            Map<TSClass, List<String>> interfaces
            ) throws OdbgenException{
        //Update superclasses.
        for(Map.Entry<TSClass, String> entry : superclasses.entrySet()){
            TSClass c = entry.getKey();
            String superclassString = entry.getValue();
            TypeDeclaration type = TypedXmlSchemaGeneratorStep1.resolveType(typeDeclarations, superclassString);
            if(type==null){
                throw new OdbgenException("Could not find type '%s' inside XmlSchema.", superclassString);
            }
            //The received type must be a class.
            if (type instanceof TSClass) {
                TSClass superclass = (TSClass) type;
                c.setSuperClass(superclass);
            }
            else{
                throw new OdbgenException("The type '%s' must be a class.", type.getQualifiedName());
            }
                
        }
        
        //Update interfaces.
        for(Map.Entry<TSClass, List<String>> entry : interfaces.entrySet()){
            TSClass c = entry.getKey();
            List<String> interfaceListString = entry.getValue();
            List<TSInterface> interfaceList = new ArrayList<TSInterface>();
            for(String interfaceString : interfaceListString){
                TypeDeclaration type = TypedXmlSchemaGeneratorStep1.resolveType(typeDeclarations, interfaceString);
                if(type==null){
                    throw new OdbgenException("Could not find type '%s' inside XmlSchema.", interfaceString);
                }
                //The received type must be an interface.
                if (type instanceof TSInterface) {
                    TSInterface interf = (TSInterface)type;
                    interfaceList.add(interf);
                }
                else{
                    throw new OdbgenException("The type '%s' must be an interface.", type.getQualifiedName());
                }
            }
            
            c.setInterfaces(interfaceList);
        }
    }

    /**
     * Receives a type name and returns the corresponding type from within
     * received type declarations. 
     * @param typeDeclarations The list of declarations used to resolve the name.
     * @param typeName The name that should be resolved into a type declaration.
     * This may be either a fully qualified name or a simple name.
     * @return The type declaration if found.
     * @throws OdbgenTypeNotFoundException If the type is not found. 
     * @throws OdbgenTypeConflictException If there is a type conflict 
     * (multiple types match but they belong to different packages). 
     */
    static TypeDeclaration resolveType(
            TypeDeclarationCollection typeDeclarations, 
            String typeName
            ) throws OdbgenTypeNotFoundException, OdbgenTypeConflictException{

        if(Utils.isQualifiedName(typeName)){
            if(typeDeclarations.contains(typeName)){
                return typeDeclarations.get(typeName);
            }
            else{
                throw new OdbgenTypeNotFoundException("Type '%s' not found.", typeName);
            }
        }
        else{
            //typeName is a simple name.
            int found = 0;
            List<TypeDeclaration> tfound = new ArrayList<TypeDeclaration>();
            for(TypeDeclaration td : typeDeclarations){
                if(td.getSimpleName().equals(typeName)){
                    found++;
                    tfound.add(td);
                }
            }
            if(found==0){
                throw new OdbgenTypeNotFoundException("Type '%s' not found.", typeName);
            }
            else if(found==1){
                return tfound.get(0);
            }
            else{
                //There are more types sharing the same simple name.
                //They differ by package name.
                
                //Build the comma separated list of
                //conflicting types.
                StringBuilder conflictingTypes = new StringBuilder();
                int k = 0;
                for(TypeDeclaration t : tfound){
                    if(k!=0){
                        conflictingTypes.append(',');
                    }
                    conflictingTypes.append(t.getQualifiedName());
                }
                
                throw new OdbgenTypeConflictException(
                        "Type conflict trying to resolve '%s'. Specify the qualified name. Follows the list of conflicting types: '%s'.",
                        typeName,
                        conflictingTypes.toString()
                ); 
            }
        }
    }

    /**
     * Builds a class given a XmlSchema element.
     * @param elem The XmlSchema element. This must represent a class.
     * @param superClasses The list of superclasses of each class inside XmlSchema.
     * The key represents the class itself and the value represents a string with the name of
     * the superclass. This will be used after all classes and interfaces are generated in order
     * to update with real superclasses and interfaces.
     * @param interfaces The same as with superclasses above.
     * @return The newly created class. It does not contain information about superclasses
     * or interfaces at this time. This information will be completed later.
     * @throws OdbgenException
     */
    private TSClass buildClass(
            Element elem, 
            Map<TSClass, String> superClasses, 
            Map<TSClass, List<String>> interfaces
            ) throws OdbgenException {
        
        
        //Name
        String simpleName = elem.getSingleChild(XmlSchemaElement.NAME.getName()).getContent();
        
        //Package
        String pkg;
        pkg = buildPackage(elem);
        
        //Fields
        List<TSField> fields = new ArrayList<TSField>();
        for(Element fieldElement : elem.getChildren(XmlSchemaElement.FIELD.getName())){
            fields.add(this.buildField(fieldElement));
        }

        TSClass c = new TSClass(
                elem,
                pkg,
                TSModifier.getPublicModifier(),
                simpleName,
                fields,
                null,
                new ArrayList<TSInterface>()
                );

        //Extends
        if(elem.hasSingleChild(XmlSchemaElement.EXTENDS.getName())){
            superClasses.put(c, elem.getSingleChild(XmlSchemaElement.EXTENDS.getName()).getContent());
        }
        
        //Implements
        if(elem.hasSingleChild(XmlSchemaElement.IMPLEMENTS.getName())){
            List<String> interfaceList = new ArrayList<String>();
            String interfacesString = elem.getSingleChild(XmlSchemaElement.IMPLEMENTS.getName()).getContent();
            for(String s : interfacesString.split(",")){
                if(s.length()>0)
                    interfaceList.add(s);
            }
            interfaces.put(c, interfaceList);
        }
        
        return c;
    }


    /**
     * Constructs a field given a XmlSchema element.
     * @param elem The XmlSchema element representing a field.
     * @throws OdbgenException
     */
    private TSField buildField(Element elem) throws OdbgenException {
        String name = elem.getSingleChild(XmlSchemaElement.NAME.getName()).getContent();
        DataType type = new DataType(elem.getSingleChild(XmlSchemaElement.FIELDTYPE.getName()).getContent());
        TSField f = new TSField(
                elem,
                TSModifier.getPrivateModifier(),
                name, 
                type
                );
        return f;
    }

    /**
     * Builds an interface given a XmlSchema element.
     * @param elem The XmlSchema element. This must represent an interface.
     * @return The newly created interface.
     * @throws OdbgenException
     */
    private TSInterface buildInterface(Element elem) throws OdbgenException {
        //Name
        String simpleName = elem.getSingleChild(XmlSchemaElement.NAME.getName()).getContent();
        //Package
        String pkg;
        pkg = buildPackage(elem);
    
        //Constants
        List<TSGetterSetter> getterSetters = new ArrayList<TSGetterSetter>();
        for(Element gsElement : elem.getChildren(XmlSchemaElement.INTERFACEGETTERSETTER.getName())){
            getterSetters.add(this.buildGetterSetter(gsElement));
        }

        TSInterface itf = new TSInterface(
                elem,
                pkg,
                TSModifier.getPublicModifier(),
                simpleName,
                getterSetters
                );
        
        return itf;
    }

    /**
     * Constructs a getter/setter given a XmlSchema element.
     * @param elem The XmlSchema element representing a getter/setter.
     * @throws OdbgenException
     */
    private TSGetterSetter buildGetterSetter(Element gsElement) throws OdbgenException {
        String baseName = gsElement.getSingleChild(XmlSchemaElement.GSBASENAME.getName()).getContent();
        DataType type = new DataType(gsElement.getSingleChild(XmlSchemaElement.GSTYPE.getName()).getContent());
        boolean hasGetter = Boolean.parseBoolean(gsElement.getSingleChild(XmlSchemaElement.GSHASGETTER.getName()).getContent());
        boolean hasSetter = Boolean.parseBoolean(gsElement.getSingleChild(XmlSchemaElement.GSHASSETTER.getName()).getContent());

        TSGetterSetter gs = new TSGetterSetter(gsElement, baseName, type, hasGetter, hasSetter);
        
        return gs;
    }

    
    /**
     * Builds an enum given a XmlSchema element.
     * @param elem The XmlSchema element. This must represent an enum.
     * @return The newly created enum.
     * @throws OdbgenException
     */
    private TSEnum buildEnum(Element elem) throws OdbgenException {
        //Name
        String simpleName = elem.getSingleChild(XmlSchemaElement.NAME.getName()).getContent();
        //Package
        String pkg;
        pkg = buildPackage(elem);
    
        //Parameters
        List<TSEnumParameter> parameters = new ArrayList<TSEnumParameter>();
        //Build a parameter map used to build later the enum values.
        //The key is the parameter id.
        Map<String, TSEnumParameter> parameterMap = new Hashtable<String, TSEnumParameter>();
        int order = 0;
        for(Element parElem : elem.getChildren(XmlSchemaElement.ENUMPARAMETER.getName())){
            parameters.add(this.buildEnumParameter(parElem, parameterMap, order));
            order++;
        }
        
        
        //Constants
        List<TSEnumConstant> constants = new ArrayList<TSEnumConstant>();
        for(Element cstElem : elem.getChildren(XmlSchemaElement.ENUMCONSTANT.getName())){
            constants.add(this.buildEnumConstant(cstElem, parameterMap, simpleName));
        }
        

        TSEnum en = new TSEnum(
                elem,
                pkg,
                TSModifier.getPublicModifier(),
                simpleName,
                constants,
                parameters
                );
        
        
        return en;
    }

    /**
     * Constructs an enum parameter given a XmlSchema element.
     * @param elem The XmlSchema element representing a enum parameter.
     * @param parameterMap When the method ends, this will hold the map between parameters
     * and parameter ids. 
     * @throws OdbgenException 
     */
    private TSEnumParameter buildEnumParameter(
            Element elem, 
            Map<String, TSEnumParameter> parameterMap,
            int order
            ) throws OdbgenException {
        String name = elem.getSingleChild(XmlSchemaElement.NAME.getName()).getContent();
        String id = elem.getSingleChild(XmlSchemaElement.ENUMPARAMETERID.getName()).getContent();
        DataType type = new DataType(elem.getSingleChild(XmlSchemaElement.ENUMPARAMETERTYPE.getName()).getContent());
        TSEnumParameter ep = new TSEnumParameter(elem, type, name, order);
        parameterMap.put(id, ep);
        return ep;
    }

    /**
     * Constructs an enum constant given a XmlSchema element.
     * @param elem The XmlSchema element representing an enum parameter.
     * @param parameterMap Holds the map between parameters and parameter id's
     * @param enumName The name of the num this constant belongs to.
     * This is used only for error reporting.
     * @throws OdbgenException 
     */
    private TSEnumConstant buildEnumConstant(Element elem, Map<String, TSEnumParameter> parameterMap, String enumName) throws OdbgenException {
        String name = elem.getSingleChild(XmlSchemaElement.NAME.getName()).getContent();
        
        List<TSEnumValue> values = new ArrayList<TSEnumValue>();
        for(Element valElem : elem.getChildren(XmlSchemaElement.ENUMVALUE.getName())){
            values.add(this.buildEnumValue(valElem, parameterMap, enumName, name));
        }
        Collections.sort(values);

        TSEnumConstant cst = new TSEnumConstant(elem, name, values);
        
        return cst;
    }

    /**
     * Constructs an enum value given a XmlSchema element.
     * @param elem The XmlSchema element representing an enum value.
     * @param parameterMap Holds the map between parameters and parameter id's
     * @param enumName The name of the enum this value belongs to. 
     * This is used only for error reporting.
     * @param constantNameThe name of the constant this value belongs to. 
     * This is used only for error reporting.
     * @return
     * @throws OdbgenException
     */
    private TSEnumValue buildEnumValue(
            Element elem, 
            Map<String, TSEnumParameter> parameterMap, 
            String enumName, 
            String constantName
            ) throws OdbgenException {

        String content = elem.getSingleChild(XmlSchemaElement.ENUMVALUECONTENT.getName()).getContent();
        String paramId = elem.getSingleChild(XmlSchemaElement.ENUMVALUEPARAMID.getName()).getContent();
        
        //Get the parameter associated to this value.
        if(!parameterMap.containsKey(paramId)){
            throw new OdbgenException(
                    "Error while creating enum constant '%s.%s': the parameter id '%s' does not exist among the list of enum parameters.",
                    enumName,
                    constantName,
                    paramId
                    );
        }
        TSEnumParameter param = parameterMap.get(paramId);

        //If the type of the parameter is 'String', add quotes if necessary.
        content = StringUtils.addQuotes(content);
        
        TSEnumValue ev = new TSEnumValue(elem, content, param);
        return ev;
    }


    /**
     * Builds a package given an XmlSchema element.
     * @param elem The element used to build the package. This must represent a type declaration.
     * @throws OdbgenException
     */
    private String buildPackage(Element elem) throws OdbgenException {
        String pkg;
        if(elem.hasSingleChild(XmlSchemaElement.PACKAGE.getName())){
            pkg = elem.getSingleChild(XmlSchemaElement.PACKAGE.getName()).getContent();
        }
        else
        {
            pkg = "";
        }
        
        //Add the root pakcage if necessary.
        if(this._rootPackage.isEmpty()){
            return pkg;
        }
        else
        {
            if(pkg.isEmpty()){
                return this._rootPackage;
            }
            else{
                return this._rootPackage+"."+pkg;
            }
            
        }
    }

}
